﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Upreader.Infrastructure.ProtoBuf
{
    public class ProtoBufDatabase
    {
        #region Defaults

        public const bool DefaultInMemoryCacheEnabled = true;

        #endregion // Defaults

        #region Fields

        private readonly string _filePath;

        private bool _inMemoryCacheEnabled = DefaultInMemoryCacheEnabled;
        private DataContext _dataContext;

        #endregion // Fields

        #region Constructor

        /// <summary>
        /// Construct a new instance with the file specified as the underlying data store
        /// </summary>
        public ProtoBufDatabase(string filePath)
        {
            if (filePath == null)
                throw new ArgumentNullException("filePath");
            if (string.IsNullOrEmpty(filePath))
                throw new ArgumentException("filePath");

            _filePath = filePath;
        }

        #endregion // Constructor

        #region Properties

        public string FilePath { get { return _filePath; } }

        /// <summary>
        /// Get or set whether inMemoryCache is enabled
        /// If enabled, save operations will save to memory prior to saving to the disk
        /// This improves performance for saving since during the saving procedure, a lock on the DataModel is required
        /// </summary>
        public bool InMemoryCacheEnabled
        {
            get { return _inMemoryCacheEnabled; }
            set { _inMemoryCacheEnabled = value; }
        }
        
        /// <summary>
        /// Get whether the current database is open
        /// </summary>
        public bool IsOpen
        {
            get { return _dataContext != null; }
        }

        #endregion // Properties

        #region Methods

        public void Open()
        {
            if (IsOpen)
                throw new InvalidOperationException("Cant open the database when already opened");

            var typeModel = ProtoBufMetaModelBuilder.Build();

            try
            {
                using (Stream stream = OpenReadStream())
                {
                    _dataContext = (DataContext)typeModel.Deserialize(stream, null, typeof(DataContext));
                }
            }
            catch (FileNotFoundException)
            {
                // initial case, construct a new DataContext.
                _dataContext = new DataContext();
            }
        }

        public void Save()
        {
            if (!IsOpen)
                throw new InvalidOperationException("Cant save the database when not opened");

            var typeModel = ProtoBufMetaModelBuilder.Build();
            var dataContext = GetDataContext();

            if (InMemoryCacheEnabled)
            {
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    dataContext.SynchronizationContext.EnterReadLock();
                    try
                    {
                        typeModel.Serialize(memoryStream, _dataContext);
                    }
                    finally
                    {
                        dataContext.SynchronizationContext.ExitReadLock();
                    }
                    using (Stream stream = OpenWriteStream())
                    {
                        dataContext.SynchronizationContext.EnterReadLock();
                        try
                        {
                            memoryStream.CopyTo(stream);
                        }
                        finally
                        {
                            dataContext.SynchronizationContext.ExitReadLock();
                        }
                    }
                }
            }
            else
            {
                using (Stream stream = OpenWriteStream())
                {
                    dataContext.SynchronizationContext.EnterReadLock();
                    try
                    {
                        typeModel.Serialize(stream, _dataContext);
                    }
                    finally
                    {
                        dataContext.SynchronizationContext.ExitReadLock();
                    }
                }
            }
        }

        /// <summary>
        /// Closes the database and optionally saves it
        /// </summary>
        public void Close(bool save)
        {
            if (!IsOpen)
                throw new InvalidOperationException("Cant close the database when not opened");

            if (save)
                Save();

            _dataContext = null;
        }

        public DataContext GetDataContext()
        {
            if (!IsOpen)
                throw new InvalidOperationException("Cant get a dataContext when not open");

            return _dataContext;
        }

        #endregion // Methods

        #region Protected methods

        protected virtual Stream OpenReadStream()
        {
            return File.Open(FilePath, FileMode.Open, FileAccess.Read);
        }

        protected virtual Stream OpenWriteStream()
        {
            return File.Open(FilePath, FileMode.CreateNew, FileAccess.Write);
        }

        #endregion // Protected methods
    }
}
